﻿//******************************************************************************************************
//  Analyst.cs - Gbtc
//
//  Copyright © 2010, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  08/02/2010 - Stephen C. Wills
//       Generated original version of source code.
//
//******************************************************************************************************

using System;
using System.IO;
using System.Threading;
using TestBenchApi.Package;
using TVA;
using TVA.PhasorProtocols;
using TVA.PhasorProtocols.IeeeC37_118;

namespace TestBenchApi
{
    /// <summary>
    /// Base class for objects that analyze the results of a test generated by the test bench.
    /// </summary>
    public abstract class Analyst
    {

        #region [ Members ]

        // Events

        /// <summary>
        /// Event that is triggered when the <see cref="Analyst"/> is started.
        /// </summary>
        public event EventHandler AnalystStarted;

        /// <summary>
        /// Event that is triggered whent he <see cref="Analyst"/> is stopped.
        /// </summary>
        public event EventHandler AnalystStopped;

        // Fields

        private PackageFile m_packageFile;
        private bool m_enabled;

        private BinaryReader m_inputFileReader;
        private BinaryReader m_outputFileReader;
        private BinaryReader m_unexpectedTimestampReader;
        private BinaryReader m_receivedTimestampReader;
        private long m_receivedTimestampPosition;

        private FrameParser m_sourceFrameParser;
        private FrameParser m_receivedFrameParser;
        private IDataFrame m_sourceFrame;
        private IDataFrame m_receivedFrame;
        private Ticks m_receivedFrameTimestamp;
        private long m_sourceFrameCount;
        private long m_nextUnexpectedTimestampOccurence;

        #endregion

        #region [ Properties ]

        /// <summary>
        /// Gets the package file used to initialize the analyst.
        /// </summary>
        public PackageFile PackageFile
        {
            get
            {
                return m_packageFile;
            }
        }

        /// <summary>
        /// Gets a value that indicates whether this analyst is running or not.
        /// </summary>
        public virtual bool Running
        {
            get
            {
                return m_enabled;
            }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Prepares the <see cref="Analyst"/> to start analyzing the test.
        /// </summary>
        /// <param name="packageFilePath">The package file used to define the test.</param>
        public virtual void Initialize(string packageFilePath)
        {
            m_packageFile = PackageFile.Load(packageFilePath);

            m_sourceFrameParser = new FrameParser(DraftRevision.Draft7);
            m_sourceFrameParser.ConfigurationFrame = TVA.PhasorProtocols.Common.DeserializeConfigurationFrame(m_packageFile.GeneratorGroup.ConfigurationFrameFile);
            m_sourceFrameParser.ReceivedFrameBufferImage += m_sourceFrameParser_ReceivedFrameBufferImage;
            m_sourceFrameParser.ReceivedDataFrame += m_sourceFrameParser_ReceivedDataFrame;

            m_receivedFrameParser = new FrameParser(DraftRevision.Draft7);
            m_receivedFrameParser.ConfigurationFrame = GetConfigurationFrame(m_packageFile.Receiver.OutputFile);
            m_receivedFrameParser.ReceivedFrameBufferImage += m_receivedFrameParser_ReceivedFrameBufferImage;
            m_receivedFrameParser.ReceivedDataFrame += m_receivedFrameParser_ReceivedDataFrame;
        }

        /// <summary>
        /// Signals the <see cref="Analyst"/> to start analyzing the test.
        /// </summary>
        public virtual void Start()
        {
            if (!m_enabled)
            {
                m_enabled = true;
                OnAnalystStarted();
                OpenFiles();

                // Read the first unexpected occurence from the timestamp file.
                m_nextUnexpectedTimestampOccurence = m_unexpectedTimestampReader.ReadInt64();

                // Start the frame parsers.
                m_sourceFrameParser.Start();
                m_receivedFrameParser.Start();

                BeginCompareFrames();
            }
        }

        /// <summary>
        /// Signals the <see cref="Analyst"/> to stop analyzing the test.
        /// </summary>
        public virtual void Stop()
        {
            if (m_enabled)
            {
                // Stop the frame parsers.
                m_sourceFrameParser.Stop();
                m_receivedFrameParser.Stop();

                CloseFiles();
                m_enabled = false;
                OnAnalystStopped();
            }
        }

        /// <summary>
        /// Triggers the event indicating that the analyst has started.
        /// </summary>
        protected virtual void OnAnalystStarted()
        {
            if (AnalystStarted != null)
                AnalystStarted(this, new EventArgs());
        }

        /// <summary>
        /// Triggers the event indicating that the analyst stopped.
        /// </summary>
        protected virtual void OnAnalystStopped()
        {
            if(AnalystStopped != null)
                AnalystStopped(this, new EventArgs());
        }

        // Opens the input, output, and timestamp files.
        private void OpenFiles()
        {
            m_inputFileReader = new BinaryReader(File.OpenRead(m_packageFile.GeneratorGroup.InputFile));
            m_outputFileReader = new BinaryReader(File.OpenRead(m_packageFile.Receiver.OutputFile));
            m_unexpectedTimestampReader = new BinaryReader(File.OpenRead(m_packageFile.Receiver.TimestampFile));
            m_receivedTimestampReader = new BinaryReader(File.OpenRead(m_packageFile.Receiver.TimestampFile));

            // Advance the timestamp readers to their proper positions in the file.
            m_receivedTimestampPosition = m_unexpectedTimestampReader.ReadInt64();
            m_receivedTimestampReader.BaseStream.Position = m_receivedTimestampPosition;
        }

        // Begins calling the consumer method to analyze the frames.
        private void BeginCompareFrames()
        {
            // Call on a separate thread so the stack can unwind.
            ThreadPool.QueueUserWorkItem(BeginCompareFrames);
        }

        // Begins calling the consumer method to analyze the frames.
        private void BeginCompareFrames(object state)
        {
            if (m_enabled)
            {
                long srcTimeMillis = (m_sourceFrame == null) ? 0 : m_sourceFrame.Timestamp / Ticks.PerMillisecond;
                long recTimeMillis = (m_receivedFrame == null) ? 0 : m_receivedFrame.Timestamp / Ticks.PerMillisecond;

                if (m_sourceFrame == null)
                    WriteSourceFrameToParser();
                else if (m_receivedFrame == null || srcTimeMillis > recTimeMillis)
                    WriteReceivedFrameToParser();
                else
                {
                    if (srcTimeMillis == recTimeMillis)
                        CompareFrames(m_sourceFrame, m_receivedFrame, m_receivedFrameTimestamp);
                    else
                        CompareFrames(m_sourceFrame, null, m_receivedFrameTimestamp);

                    WriteSourceFrameToParser();
                }
            }
        }

        // Writes a source frame into the corresponding frame parser.
        private void WriteSourceFrameToParser()
        {
            // The source file loops when sending frames to the concentrator,
            // so loop the source file when analyzing the results.
            if (m_inputFileReader.BaseStream.Position == m_inputFileReader.BaseStream.Length)
                m_inputFileReader.BaseStream.Seek(0, SeekOrigin.Begin);

            WriteFrameToParser(m_inputFileReader, m_sourceFrameParser);
        }

        // Writes a received frame into the corresponding frame parser.
        private void WriteReceivedFrameToParser()
        {
            // If we reach the end of the received frames, stop analyzing.
            if (m_outputFileReader.BaseStream.Position == m_outputFileReader.BaseStream.Length)
                Stop();
            else
                WriteFrameToParser(m_outputFileReader, m_receivedFrameParser);
        }

        // Writes a frame from the given file into the given parser.
        private void WriteFrameToParser(BinaryReader frameFile, FrameParser parser)
        {
            int headerLength = CommonFrameHeader.FixedLength;
            byte[] headerBytes, frameBytes;
            CommonFrameHeader header;

            headerBytes = frameFile.ReadBytes(headerLength);
            header = new CommonFrameHeader(null, headerBytes, 0);
            frameBytes = headerBytes.Combine(frameFile.ReadBytes(header.FrameLength - headerLength));
            parser.Write(frameBytes, 0, frameBytes.Length);
        }

        // Closes the input, output, and timestamp files.
        private void CloseFiles()
        {
            if (m_inputFileReader != null)
                m_inputFileReader.Close();

            if (m_outputFileReader != null)
                m_outputFileReader.Close();

            if (m_receivedTimestampReader != null)
                m_receivedTimestampReader.Close();
        }

        /// <summary>
        /// Compares a source frame with the corresponding received frame to determine if the test passed or failed.
        /// </summary>
        /// <param name="sourceFrame">A data frame from the source file.</param>
        /// <param name="receivedFrame">The corresponding data frame received from the concentrator during the test.</param>
        /// <param name="timeReceived">The time that the frame was received from the concentrator.</param>
        public abstract void CompareFrames(IDataFrame sourceFrame, IDataFrame receivedFrame, long timeReceived);

        // Gets the configuration frame stored in a sample data file.
        private IConfigurationFrame GetConfigurationFrame(string frameFile)
        {
            MultiProtocolFrameParser parser = new MultiProtocolFrameParser();
            ManualResetEvent waitHandle = new ManualResetEvent(false);
            IConfigurationFrame configFrame = null;

            EventHandler<EventArgs<IConfigurationFrame>> eventHandle = (obj, args) =>
            {
                parser.Stop();
                configFrame = args.Argument;
                waitHandle.Set();
            };

            parser.ConnectionString = string.Format("phasorProtocol=IeeeC37_118V1; transportProtocol=File; file={0}", frameFile);
            parser.ExecuteParseOnSeparateThread = true;
            parser.ReceivedConfigurationFrame += eventHandle;
            parser.Start();

            waitHandle.WaitOne();
            parser.ReceivedConfigurationFrame -= eventHandle;

            return configFrame;
        }

        // Processes a data frame from the source file.
        private void m_sourceFrameParser_ReceivedDataFrame(object sender, EventArgs<DataFrame> e)
        {
            m_sourceFrameCount++;

            if (m_nextUnexpectedTimestampOccurence == m_sourceFrameCount && m_unexpectedTimestampReader.BaseStream.Position < m_receivedTimestampPosition)
            {
                e.Argument.Timestamp = m_unexpectedTimestampReader.ReadInt64();
                m_nextUnexpectedTimestampOccurence = m_unexpectedTimestampReader.ReadInt64();
            }
            else
            {
                long ticks = m_sourceFrame.Timestamp;
                long ticksBeyondSecond = ticks % Ticks.PerSecond;
                long baseTicks = ticks - ticksBeyondSecond;
                int frameRate = m_packageFile.GeneratorGroup.FrameRate;
                int frameIndex = (int)Math.Ceiling((double)ticksBeyondSecond * frameRate / Ticks.PerSecond) + 1;

                ticksBeyondSecond = (frameIndex * 1000 / frameRate) * Ticks.PerMillisecond;
                e.Argument.Timestamp = baseTicks + ticksBeyondSecond;
            }

            m_sourceFrame = e.Argument;
            BeginCompareFrames();
        }

        // Writes the next frame into the parser if the received frame is not a data frame.
        private void m_sourceFrameParser_ReceivedFrameBufferImage(object sender, EventArgs<FundamentalFrameType, byte[], int, int> e)
        {
            if (e.Argument1 != FundamentalFrameType.DataFrame)
                WriteSourceFrameToParser();
        }

        // Writes the next frame into the parser if the received frame is not a data frame.
        private void m_receivedFrameParser_ReceivedFrameBufferImage(object sender, EventArgs<FundamentalFrameType, byte[], int, int> e)
        {
            if (e.Argument1 != FundamentalFrameType.DataFrame)
            {
                // Discard the frame's timestamp and
                // write the next frame to the parser.
                m_receivedTimestampReader.ReadInt64();
                WriteReceivedFrameToParser();
            }
        }

        // Processes a data frame from the received frames file.
        private void m_receivedFrameParser_ReceivedDataFrame(object sender, EventArgs<DataFrame> e)
        {
            m_receivedFrame = e.Argument;
            m_receivedFrameTimestamp = m_receivedTimestampReader.ReadInt64();

            BeginCompareFrames();
        }

        #endregion
        
    }
}
